package phidgetmotion.classifier;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.TreeMap;

import com.phidgets.event.DetachEvent;
import com.phidgets.event.DetachListener;

import phidgetmotion.phidget.Accelerometer;
import phidgetmotion.phidget.FeatureGenerator;

public class Model extends AbstractClassifier {
	private LinkedList<String> classNames;
	private ArrayList<FeatureVector> dataSet;
	private int updateInterval;
	private int kNearest = 1;
	private ModelTrainer currentTrainer = null;
	
	private FeatureGenerator featureGenerator;
	private FeatureInputListener classifier = new FeatureInputListener() {
		
		@Override
		public void featureInput(FeatureVector vector) {
			TreeMap<Double, Integer> map = new TreeMap<Double, Integer>();
			Iterator<FeatureVector> iter = dataSet.iterator();
			while(iter.hasNext()){
				FeatureVector vec = iter.next();
				map.put(vector.distance(vec), vec.getClassLabel());
			}
			int[] classesCount = new int[classNames.size()];
			Iterator<Integer> classIter = map.values().iterator();
			int i=0;
			while(classIter.hasNext() && i<kNearest){
				classesCount[classIter.next()]++;
				i++;
			}
			int max = 0;
			int predictedClassId = -1;
			for(i=0; i<classNames.size(); i++){
				if(classesCount[i]>max){
					max = classesCount[i];
					predictedClassId = i;
				}
			}
			update(predictedClassId);
		}
	};
	
	private DetachListener detachListener = new DetachListener() {
		@Override
		public void detached(DetachEvent arg0) {
			update(-1);		
		}
	};
	
	public class ModelTrainer{
		private int classId;
		private int maxTrainigsSamples;
		private boolean finished = false;
		private int count = 0;
		private LinkedList<ModelTrainerListener> listener = new LinkedList<ModelTrainerListener>();
		
		private FeatureInputListener trainer = new FeatureInputListener() {
			
			@Override
			public void featureInput(FeatureVector vector) {
				if(getVectorsCount(classId) >= maxTrainigsSamples) stop();
				else{
					if(count>0){
						vector.setClassLabel(classId);
						addFeatureVector(vector);
						fireUpdated();
					}
					count++;
				}
			}
		};
		
		private DetachListener detachListener = new DetachListener() {
			@Override
			public void detached(DetachEvent arg0) {
				stop();			
			}
		};
		
		private ModelTrainer(int classId, int maxTrainigsSamples) {
			if(classId>=classNames.size()) throw new IllegalArgumentException("classId unknown");
			this.classId = classId;
			this.maxTrainigsSamples = maxTrainigsSamples;
			if(getVectorsCount(classId) >= maxTrainigsSamples || !Accelerometer.isConnected()){
				currentTrainer = null;
				finished = true;
				fireFinisheded();
			}
			else {
				featureGenerator.addFeatureInputListener(trainer);
				Accelerometer.addDetachListener(detachListener);
			}
			
		}
		
		public void stop(){
			Accelerometer.removeDetachListener(detachListener);
			featureGenerator.removeFeatureInputListener(trainer);
			currentTrainer = null;
			finished = true;
			fireFinisheded();
		}
		
		private void fireUpdated(){
			Iterator<ModelTrainerListener> iter = listener.iterator();
			while(iter.hasNext()){
				iter.next().updated(classId);
			}
		}
		
		private void fireFinisheded(){
			Iterator<ModelTrainerListener> iter = listener.iterator();
			while(iter.hasNext()){
				iter.next().finished(classId);
			}
		}

		public boolean isFinished() {
			return finished;
		}
		
		public void addModelTrainerListener(ModelTrainerListener l){
			listener.add(l);
		}
		public void removeModelTrainerListener(ModelTrainerListener l){
			listener.remove(l);
		}
	}
	
	public Model(int updateInterval) {
		this.updateInterval = updateInterval;
		classNames = new LinkedList<String>();
		dataSet = new ArrayList<FeatureVector>();
		init();
	}
	
	@SuppressWarnings("unchecked")
	public Model(File file) throws IOException {
		ObjectInputStream oIn = new ObjectInputStream(new FileInputStream(file));
		try {
			classNames = (LinkedList<String>) oIn.readObject();
			dataSet = (ArrayList<FeatureVector>) oIn.readObject();
			updateInterval = oIn.readInt();
			kNearest = oIn.readInt();
		} catch (ClassNotFoundException e) {
			e.printStackTrace();
		}
		oIn.close();
		init();
	}
	
	private void init(){
		featureGenerator = new FeatureGenerator(updateInterval);
		featureGenerator.addFeatureInputListener(classifier);
		Accelerometer.addDetachListener(detachListener);
	}
	
	public void close() {
		featureGenerator.removeFeatureInputListener(classifier);
		Accelerometer.removeDetachListener(detachListener);
		featureGenerator.close();
	}
	
	public void save(File file) throws IOException{
		ObjectOutputStream oOut = new ObjectOutputStream(new FileOutputStream(file));
		oOut.writeObject(classNames);
		oOut.writeObject(dataSet);
		oOut.writeInt(updateInterval);
		oOut.writeInt(kNearest);
		oOut.close();
	}

	public int addClass(String name){
		classNames.add(name);
		return classNames.size()-1;
	}
	
	public void removeClass(int classId){
		if(classId<0 || classId >= classNames.size()) throw new IllegalArgumentException("The given class was not defined");
		removeData(classId);
		classNames.remove(classId);
	}
	
	public void removeClass(String name){
		int index = classNames.indexOf(name);
		if(index==-1) throw new IllegalArgumentException("The given class was not defined");
		removeData(index);
		classNames.remove(name);
	}
	
	@Override
	public String[] getClasses(){
		return classNames.toArray(new String[0]);
	}
	
	public ModelTrainer getModelTrainer(int classId, int maxTrainigsSamples){
		if(currentTrainer!=null) throw new IllegalStateException("Only one trainer allowed at the same time");
		currentTrainer = new ModelTrainer(classId, maxTrainigsSamples);
		return currentTrainer;
	}
	
	private void addFeatureVector(FeatureVector vector){
		dataSet.add(vector);
	}
	
	public void removeData(int classId){
		for(int i=0; i<dataSet.size(); i++){
			if(dataSet.get(i).getClassLabel()==classId){
				dataSet.remove(i);
				i--;
			}
		}
	}
	
	public boolean containsData(){
		return dataSet.size()>0;
	}
	
	public void removeAllData(){
		dataSet.clear();
	}
	
	public int getVectorsCount(int classId){
		if(classId>=classNames.size()) throw new IllegalArgumentException("classId unknown");
		int sum = 0;
		Iterator<FeatureVector> iter = dataSet.iterator();
		while(iter.hasNext()){
			if(iter.next().getClassLabel()==classId) sum++;
		}
		return sum;
	}
	
	public int getUpdateInterval() {
		return updateInterval;
	}

	public void setUpdateInterval(int updateInterval) {
		if(updateInterval<=0) throw new IllegalArgumentException("updateInterval must be greater than 0");
		if(this.updateInterval != updateInterval){
			if(currentTrainer!=null) throw new IllegalStateException("Cannot set updateInterval when model is trained");
			if(dataSet.size()>0) throw new IllegalStateException("Cannot set updateInterval when model contains data");
			this.updateInterval = updateInterval;
			featureGenerator.setUpdateInterval(updateInterval);
		}
	}
	
	public int getkNearest() {
		return kNearest;
	}

	public void setkNearest(int kNearest) {
		if(kNearest<=0) throw new IllegalArgumentException("kNearest must be greater than 0");
		this.kNearest = kNearest;
	}
	
	private static Model instance = null;
	private static LinkedList<ModelChangeListener> modelChangeListener = new LinkedList<ModelChangeListener>();
	
	public static void addModelChangeListener(ModelChangeListener l){
		modelChangeListener.add(l);
		if(instance!=null) l.modelAdded(instance);
	}
	
	public static void removeModelChangeListener(ModelChangeListener l){
		modelChangeListener.remove(l);
	}
	
	private static void fireModelAdded(){
		Iterator<ModelChangeListener> iter = modelChangeListener.iterator();
		while(iter.hasNext()){
			iter.next().modelAdded(instance);
		}
	}
	
	private static void fireModelRemoved(){
		Iterator<ModelChangeListener> iter = modelChangeListener.iterator();
		while(iter.hasNext()){
			iter.next().modelRemoved();
		}
	}
	
	public static Model create(int updateInterval) {
		if(updateInterval<=0) throw new IllegalArgumentException("updateInterval must be greater than 0");
		if(instance!=null) {
			fireModelRemoved();
			instance.close();
		}
		instance = new Model(updateInterval);
		fireModelAdded();
		return instance;
	}
	
	public static Model load(File file) throws IOException{
		if(instance!=null) {
			fireModelRemoved();
			instance.close();
		}
		instance = new Model(file);
		fireModelAdded();
		return instance;
	}
	
	public static Model getModel(){
		return instance;
	}
	
	public static Classifier getClassifier(){
		return getModel();
	}
}
